/*
 * Copyright (C) 2014 Allwinner Ltd.
 *
 * Author:
 *	Ryan Chen <ryanchen@allwinnertech.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, version 2 of the
 * License.
 *
 * File: fiv_main.c
 */
#include <linux/module.h>
#include <linux/file.h>
#include <linux/binfmts.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include <linux/err.h>
#include <linux/delay.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/crypto.h>
#include <linux/crc32.h>
#include <linux/bsearch.h>
#include <linux/spinlock.h>
#include <linux/fivm.h>

#include "fivm.h"

int fivm_debug;

static int fivm_initialized;
static struct FILE_SIG *file_sig_table;
static struct FILE_SIG_HEAD *file_sig_head;
static const char ROOT[] = "/system";

DEFINE_MUTEX(_fid_mutex);

#ifdef FIVM_DEBUG_TIMMING
#include <linux/time.h>
static void fivm_gettime(const char *str)
{
	struct timeval	t;
	do_gettimeofday(&t);
	dprintk("[FIVM timer]%s: %lu.%lu\n", str, t.tv_sec, t.tv_usec);
}
#else
static void fivm_gettime(const char *str)
{
	return;
}
#endif

static char _p_dir[1024];
static int __walk_to_root(struct file *file, char *pathname,
			struct fivm_path *search_fp)
{
	struct dentry *p;
	char *start,
		 *f_path;
	int rc = -1;
	int loop, len;

	mutex_lock(&_fid_mutex);
	p = file->f_dentry;

	start = _p_dir + sizeof(_p_dir) - 1; /*Reserved a null for string end*/
	memset(_p_dir, 0, sizeof(_p_dir));
	for (loop = 0; loop < 16 && p; loop ++, p = p->d_parent) {
		if (*(p->d_name.name) == '/')
			break;
		else{
			len = strnlen(p->d_name.name, MAX_NAME_LEN);
			start -= len;
			memcpy(start, p->d_name.name, len);
			*(--start) = '/';
		}

	}

	dprintk("FIVM find root path : %s\n", start);

	if (!memcmp(start, ROOT, sizeof(ROOT)-1)) {
		/*full file path name*/
		len = strnlen(start, FILE_NAME_LEN);
		f_path = kmalloc(len, GFP_KERNEL);
		if (!f_path) {
			derr("Out of memory\n");
			goto out;
		}
		memcpy(f_path, start, len);
		search_fp->path = f_path;
		search_fp->flag = FIVM_PART_PATH;
		rc = 0;
	}

out:
	mutex_unlock(&_fid_mutex);
	return rc;

}

/*
 *
 */
static int _find_dir(struct file *file, char *pathname,
		struct fivm_path *search_fp)
{

	if (unlikely(!pathname)) {
		dprintk("Null file path\n");
		return -1;
	}

	if (likely(*pathname == '/'))
		do {
			if (!memcmp(pathname, ROOT, sizeof(ROOT)-1)) {
				/*full file path name*/
				search_fp->path = pathname;
				search_fp->flag = FIVM_FULL_PATH;
				dprintk("Root File path: %s\n", pathname);
				return 0;
			}
		} while (0);
	else
		return __walk_to_root(file, pathname, search_fp);

	return -1;
}

/*Incremental crc table*/
static int _cmp(const void *a, const void *b)
{
	const int *fa = a;
	const struct FILE_SIG *fb = b;

	if (*fa < fb->crc)
		return -1;
	if (*fa > fb->crc)
		return 1;
	return 0;
}

static struct FILE_SIG *_find_file(char *file_name)
{
	struct FILE_SIG *fs;
	int crc, slen;
	size_t num, size;
	void *base;

	base = file_sig_table;
	num = file_sig_head->actual_cnt;
	size = sizeof(struct FILE_SIG);

	slen = strnlen(file_name, FILE_NAME_LEN);
	crc = ~crc32_le(~0, file_name, slen);
	dprintk("Find_file debug: name %s, crc 0x%x", file_name, crc);

	fs = bsearch(&crc, base, num, size, _cmp);
	if (!fs) {
		derr("Find file %s crc fail\n", file_name);
		return NULL;
	}

	if (strncmp(file_name, fs->name, slen)) {
		derr("Find file %s name fail\n", file_name);
		return NULL;
	}

	return fs;
}

static void rel_fivm_path(struct fivm_path *fp)
{
	if (fp->flag == FIVM_PART_PATH &&
			fp->path)
		kfree(fp->path);
}

static int process_verify(struct file *file,  char *pathname)
{
	struct inode *inode = file->f_dentry->d_inode;
	struct FILE_SIG *fs;
	int rc = 0;
	char digest[SHA_DIG_MAX];

	struct fivm_path search_fp = { NULL, 0};

	fivm_gettime("Enter process verication");

	if (!fivm_initialized || !S_ISREG(inode->i_mode))
		return 0;

	rc = _find_dir(file, pathname, &search_fp);
	if (rc != 0)
		/*Nothing to do, Just go on */
		return 0;

	fivm_gettime("Find directory name ");
	fs = _find_file(search_fp.path);
	if (!fs) {
		derr("Can't find file %s in sha table\n", search_fp.path);
		goto out;
	}

	if (fs->flag) {
		dprintk("Had verified\n");
		goto out;
	}

	fivm_gettime("Find file name ");

	memset(digest, 0, SHA_DIG_MAX);
	rc = fivm_calc_hash(file, digest);
	if (rc) {
		derr("fivm_calc_hash fail\n");
		goto out;
	}

	fivm_gettime("Calc file digest ");
	if (memcmp(digest, fs->sha, SHA_DIG_MAX)) {
		derr("Sha compare fail\n");
		derr("Local calculate:\n");
		print_hex_dump_bytes("", DUMP_PREFIX_NONE,
				digest, SHA_DIG_MAX);
		derr("Store value:\n");
		print_hex_dump_bytes("", DUMP_PREFIX_NONE,
				fs->sha, SHA_DIG_MAX);
		rc = -1;
		goto out;
	}
	fs->flag = 1;
	rc = 0;
out:
	rel_fivm_path(&search_fp);
	return rc;
}


/**
 * fivm_file_mmap
 * @file: pointer to the file to be verify
 * @prot: contains the protection that will be applied by the kernel.
 *
 * Return 0 on success, an error code on failure.
 */
static int _fivm_mmap_verify(struct file *file, unsigned long prot)
{
	int rc;

	if (!file)
		return 0;

	rc = process_verify(file, (char *)file->f_dentry->d_name.name);
	return rc;
}

/**
 * fivm_open_verify - verify file against to the hash table
 * @file: pointer to the file to be verify
 * @mask:
 *
 * Return 0 on success, an error code on failure.
 */
static int _fivm_open_verify(struct file *file, const char *pathname, int mask)
{
	int rc;
	if (!file || !pathname)
		return 0;

	rc = process_verify(file, (char *)pathname);
	return rc;
}

#ifdef FIVM_LKM_DEBUG
static int _file_meta_read(
		char *filename,
		char *buf,
		ssize_t len,
		int offset
		)
{
	struct file *fd;
	int retlen = -1;
	mm_segment_t old_fs;

	if (!filename || !buf) {
		derr("- filename/buf NULL\n");
		return -EINVAL;
	}

	old_fs = get_fs();
	set_fs(KERNEL_DS);

	fd = filp_open(filename, O_RDONLY, 0);
	if (IS_ERR(fd)) {
		derr(" -file open fail\n");
		return -1;
	}
	do {
		if ((fd->f_op == NULL) || (fd->f_op->read == NULL)) {
			derr(" -file can't to open!!\n");
			break;
		}

		if (fd->f_pos != offset) {
			if (fd->f_op->llseek) {
				if (fd->f_op->llseek(fd, offset, 0) != offset) {
					derr(" -failed to seek!!\n");
					break;
				}
			} else {
				fd->f_pos = offset;
			}
		}

		retlen = fd->f_op->read(fd,
				buf,
				len,
				&fd->f_pos);
	} while (false);

	filp_close(fd, NULL);
	set_fs(old_fs);

	return retlen;
}

static const char *sig_table = "/dev/block/by-name/verity_block";
static int read_file_hash_table(void)
{
	int cnt, rlen,
		rc = -1,
		offset;

	file_sig_head = kzalloc(sizeof(*file_sig_head),
			GFP_KERNEL);
	if (!file_sig_head) {
		derr(" out of memory\n");
		return -ENOMEM;
	}

	rlen = sizeof(struct FILE_SIG_HEAD);
	offset = sizeof(struct FILE_LIST_HEAD);
	rc = _file_meta_read(
			(char *)sig_table,
			(char *)file_sig_head,
			rlen,
			offset);
	if (rc != rlen || file_sig_head->magic != FILE_SIG_MAGIC) {
		derr("Read file %s fail\n", sig_table);
		rc = -EIO;
		goto out;
	}

	cnt = file_sig_head->actual_cnt;
	if (cnt > DIR_MAX_FILE_NUM) {
		derr("out of files count 0x%x\n", cnt);
		rc = -1;
		goto out;
	}

	rlen = sizeof(struct FILE_SIG)*cnt;
	file_sig_table = kzalloc(rlen, GFP_KERNEL);
	if (!file_sig_table) {
		derr(" out of memory\n");
		rc = -ENOMEM;
		goto out;
	}

	offset += sizeof(*file_sig_head);
	rc = _file_meta_read(
			(char *)sig_table,
			(char *)file_sig_table,
			rlen,
			offset
			);

	if (rc != rlen) {
		derr("Read file %s fail\n", sig_table);
		rc = -EIO;
		goto out;
	}

	rc = 0;
out:
	if (file_sig_table)
		kfree(file_sig_table);
	if (file_sig_head)
		kfree(file_sig_head);

	return rc;
}
#endif

extern int fivm_register_func(
		int (*mmap_verify)(struct file *, unsigned long),
		int (*open_verify)(struct file *, const char *, int)
		);
extern int fivm_unregister_func(void);

raw_spinlock_t fivm_hook_lock;
int fivm_enable(void)
{
#ifdef FIVM_LKM_DEBUG
	int rc;
	rc = read_file_hash_table();
	if (rc < 0) {
		derr("read hash table fail\n");
		return -1;
	}
#endif
	raw_spin_lock(&fivm_hook_lock);
	fivm_register_func(_fivm_mmap_verify, _fivm_open_verify);
	fivm_initialized = 1;
	raw_spin_unlock(&fivm_hook_lock);

	pr_info("FIVM: enable fivm done\n");
	return 0;
}

int  fivm_disable(void)
{
	raw_spin_lock(&fivm_hook_lock);
	fivm_unregister_func();
	fivm_initialized = 0;
	raw_spin_unlock(&fivm_hook_lock);
	return 0;
}

/*Get fivm table from user*/
int fivm_set(void *arg)
{
	struct fivm_param param;
	void *sh, *st;
	unsigned int sh_size, st_size;

	if (!access_ok(VERIFY_READ, arg, sizeof(param))) {
		derr("fivm param error\n");
		return -EFAULT;
	}

	memcpy(&param, (void *)arg, sizeof(param));

	sh = param.sig_head;
	sh_size = param.sig_head_size;
	st = param.sig_table;
	st_size = param.sig_table_size;

	if (sh_size != sizeof(*file_sig_head) ||
		st_size > FILE_NAME_LEN * DIR_MAX_FILE_NUM) {
		derr("fivm set buf size error %d:%d\n",
				sh_size, st_size);
		return -EFAULT;
	}

	file_sig_head = kzalloc(sh_size, GFP_KERNEL);
	if (!file_sig_head || file_sig_table) {
		derr("out of memory\n");
		return -EFAULT;
	}

	if (copy_from_user(file_sig_head, (void __user *) sh, sh_size)) {
		derr("fivm sig head buffer from user fail\n");
		kfree(file_sig_head);
		return -EFAULT;
	}

	if (st_size != file_sig_head->actual_cnt * sizeof(struct FILE_SIG)) {
		derr("wrong fivm sig table count\n");
		kfree(file_sig_head);
		return -EFAULT;
	}
	file_sig_table = kzalloc(st_size, GFP_KERNEL);
	if (!file_sig_table) {
		derr("out of memory file_sig_table\n");
		kfree(file_sig_head);
		return -EFAULT;
	}

	if (copy_from_user(file_sig_table, (void __user *)st, st_size)) {
		derr("fivm sig table buffer from user fail\n");
		kfree(file_sig_head);
		kfree(file_sig_table);
		return -EFAULT;
	}

	if (fivm_debug == 1) {
		print_hex_dump_bytes("fivm io sig head buf:", DUMP_PREFIX_NONE,
				file_sig_head, sh_size);
		print_hex_dump_bytes("fivm io sig table buf:", DUMP_PREFIX_NONE,
				file_sig_table, 16 * sizeof(struct FILE_SIG));
	}
	return 0;
}

int fivm_init(void)
{
	pr_info("FIVM init\n");
	raw_spin_lock_init(&fivm_hook_lock);
	return 0;
}

int fivm_cleanup(void)
{
	fivm_disable();
	if (file_sig_head)
		kfree(file_sig_head);
	if (file_sig_table)
		kfree(file_sig_table);
	return 0;
}


